#!/usr/bin/env python
# panpipe - automated WAV image compression
# Copyright (C) 2006 Iulian Ciulinet

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import sys, re, win32process, win32con, win32event, string, os, fnmatch, shutil
import md5, binascii
from optparse import OptionParser
from ConfigParser import SafeConfigParser
from os.path import *
from tempfile import *

_PANPIPE_VERSION = '0.0.2'
_PANPIPE_LICENSE_BLURB = """Panpipe version """ + _PANPIPE_VERSION + """, Copyright (C) 2007 Iulian Ciulinet
Panpipe comes with ABSOLUTELY NO WARRANTY. This is free software,
and you are welcome to redistribute it under the terms of the GNU
General Public License. For more information, see the file COPYING.
"""


def fixfilename(strr, blank = False):
    if blank:
        tr = string.maketrans('\\/:*?"<>|', "         ")
    else:
        tr = string.maketrans('\\/:*?"<>|', ",,-x '[]!")
    return strr.translate(tr)


def win32_exec(cl, verbose = False):
    if verbose:
        print
        print "Launching external command..."
        print cl
        print
    psi = win32process.STARTUPINFO()
    hp = win32process.CreateProcess(None, cl, None, None, True,
            win32con.NORMAL_PRIORITY_CLASS, None, None, psi)
    obj = win32event.WaitForSingleObject(hp[0], win32event.INFINITE)
    ret = win32process.GetExitCodeProcess(hp[0])

    if ret != 0:
        print "Error: external command failed."
        sys.exit(1)

    return ret


def morph_cue(src_fn, tgt_fn, image):
    """
    Transcribes a cuesheet file from file <src_fn> to file <tgt_fn>,
    and updates the image file name with <image>.
    """
    src_file = file(src_fn, 'r')
    tgt_file = file(tgt_fn, 'w')
    img_rx = re.compile("^FILE \"(.*)\"")
    for line in src_file.readlines():
        if img_rx.match(line):
            tgt_file.write('FILE "' + image + '" WAVE\n')
        else:
            tgt_file.write(line)
    tgt_file.close()
    src_file.close()


class CueInfo:
    def __init__(self, filename):
        self.filename = filename
        self.album_artist = "Unknown Artist"
        self.album_title = "Untitled Album"
        self.src_imgfile = "unknown_file"
        self.track_list = []
        self.compilation = False
        self.parse()

    def parse(self):
        try:
            cuefile = file(self.filename)
        except:
            print "Error reading CUE file", self.filename
            sys.exit(1)

        alb_art_rx = re.compile("^PERFORMER \"(.*)\"")
        alb_tit_rx = re.compile("^TITLE \"(.*)\"")
        img_fil_rx = re.compile("^FILE \"(.*)\"")
        trk_num_rx = re.compile("^  TRACK ([0-9]+) AUDIO")
        trk_art_rx = re.compile("^    PERFORMER \"(.*)\"")
        trk_tit_rx = re.compile("^    TITLE \"(.*)\"")

        for line in cuefile.readlines():
            m = alb_art_rx.match(line)
            if m:
                self.album_artist = m.group(1)
                continue
            m = alb_tit_rx.match(line)
            if m:
                self.album_title = m.group(1)
                continue
            m = img_fil_rx.match(line)
            if m:
                self.src_imgfile = m.group(1)
                continue
            m = trk_num_rx.match(line)
            if m:
                self.track_list.append({'number': int(m.group(1)),
                    'artist': 'Unknown Artist',
                    'title': 'Untitled Song'})
                continue
            m = trk_art_rx.match(line)
            if m:
                self.track_list[-1]['artist'] = m.group(1)
                if self.track_list[-1]['artist'] != self.album_artist:
                    self.compilation = True
                continue
            m = trk_tit_rx.match(line)
            if m:
                self.track_list[-1]['title'] = m.group(1)
                continue
        cuefile.close()

    def dump(self):
        print "Cue file info", self.filename
        print "  Artist:", self.album_artist
        print "  Album:", self.album_title
        print "  File:", self.src_imgfile
        for t in self.track_list:
            print "    Track", t['number'], ':', t['artist'], '-', t['title']
        if self.compilation:
            print "Album is a compilation"
        else:
            print "Album is not a compilation"


class PPApp:
    def __init__(self):
        self.get_args()
        self.init_first()
        self.init_tags()
        self.init_config()
        self.run()
        self.cleanup()

    def get_args(self):
        opt_parser = OptionParser(usage="%prog -c config_file [-h]" +
                " [-y YEAR] [-g GENRE] [-e] [-v] image_file")
        opt_parser.add_option("-c", "--config_file",
                action="store", type="string", dest="cfg_file",
                help="configuration file")
        opt_parser.add_option("-y", "--year",
                action="store", type="string", dest="year",
                help="release year")
        opt_parser.add_option("-g", "--genre",
                action="store", type="string", dest="genre",
                help="music genre")
        opt_parser.add_option("-e", "--eac", action="store_true",
                dest="eac", default=False,
                help="EAC compatible mode (default: off)")
        opt_parser.add_option("-v", "--verbose", action="store_true",
                dest="verbose", default=False,
                help="print lots of messages (default: off)")
        (self.options, self.args) = opt_parser.parse_args()

        if not self.args:
            opt_parser.error("no image file specified.")

        if not self.options.cfg_file:
            opt_parser.error("no configuration file specified.")

    def init_first(self):
        self.src_imgfile = abspath(self.args[0])
        self.src_dir = dirname(self.src_imgfile)

        if self.options.eac:
            dir_list = os.listdir(self.src_dir)
            cue_list = fnmatch.filter(dir_list, '*.cue')
            if len(cue_list) == 0:
                print "Error: no cue file found."
                sys.exit(1)
            else:
                self.orig_cuefile = join(self.src_dir, cue_list[0])

            log_list = fnmatch.filter(dir_list, '*.log')
            if len(log_list) == 0:
                self.src_logfile = ""
            else:
                self.src_logfile = join(self.src_dir, log_list[0])
        else:
            # CUE and LOG files are required to have the same name
            self.orig_cuefile = splitext(self.src_imgfile)[0] + '.cue'
            self.src_logfile = splitext(self.src_imgfile)[0] + '.log'

        if not isfile(self.orig_cuefile):
            print "Error: cannot found CUE file.", self.src_cuefile
            sys.exit(1)

        self.src_cuefile = mktemp('.cue', 'pp_', self.src_dir)
        morph_cue(self.orig_cuefile, self.src_cuefile,
                basename(self.src_imgfile))


    def init_tags(self):
        self.cue = CueInfo(self.src_cuefile)

        if self.options.verbose:
            self.cue.dump()
            print

        self.tag_compilation = self.cue.compilation
        self.tag_album_artist = self.cue.album_artist
        self.tag_album_title = self.cue.album_title
        self.tag_year = self.options.year or '2006'
        self.tag_genre = self.options.genre or 'Unknown Genre'
        self.tag_comment = None # filled from config file
        self.tag_tracks = self.cue.track_list

    def init_config(self):
        self.config = SafeConfigParser()
        self.config.readfp(file(self.options.cfg_file))
        self.tag_comment = self.config.get('general', 'comment')
        self.config_vars = {
                'genre': self.tag_genre,
                'album_artist': self.tag_album_artist,
                'year': self.tag_year,
                'album_title': self.tag_album_title,
                'comment': self.tag_comment,
                'track_number': None,
                'track_artist': None,
                'track_title': None,
                'f_genre': fixfilename(self.tag_genre),
                'f_album_artist': fixfilename(self.tag_album_artist),
                'f_year': fixfilename(self.tag_year),
                'f_album_title': fixfilename(self.tag_album_title),
                'f_comment': fixfilename(self.tag_comment),
                'f_track_number': None,
                'f_track_artist': None,
                'f_track_title': None,
                'f_infile': None,
                'f_infile_list': None,
                'f_outfile': None,
                'f_cuefile': None
                }

    def run(self):
        self.wd = mkdtemp('.tmp', 'pp_', self.src_dir)
        self.flac_type = self.config.get('general', 'flac_type')
        self.lame_type = self.config.get('general', 'lame_type')
        if self.flac_type == 'Image' or self.flac_type == 'Both':
            self.encode_image('flac')
        if self.flac_type == 'Tracks' or self.flac_type == 'Both' \
                or self.lame_type == 'Tracks' or self.lame_type == 'Both':
            self.split_image()
            if self.flac_type == 'Tracks' or self.flac_type == 'Both':
                self.encode_tracks('flac')
            if self.lame_type == 'Tracks' or self.lame_type == 'Both':
                self.encode_tracks('mp3')
        if self.lame_type == 'Image' or self.lame_type == 'Both':
            self.encode_image('mp3')

    def cleanup(self):
        shutil.rmtree(self.wd, True) # ignore errors
        os.remove(self.src_cuefile)
        if self.config.get('general', 'delete_image') == 'Yes':
            os.remove(self.src_imgfile)
        if self.config.get('general', 'delete_cue') == 'Yes':
            os.remove(self.orig_cuefile)
        if self.config.get('general', 'delete_image') == 'Yes' \
                and isfile(self.src_logfile):
            os.remove(self.src_logfile)

    def encode_image(self, type):
        ext = '.' + type               # file extension
        sect = type + '_image'         # config section
        exe = type + '_exe'            # setting name for codec executable
        rgexe = type + '_rgexe'        # setting name for replaygain executable

        tmp_dest = mktemp(ext, 'pp_', self.wd)

        self.config_vars['f_infile'] = self.src_imgfile
        self.config_vars['f_outfile'] = tmp_dest
        self.config_vars['f_cuefile'] = self.src_cuefile

        if self.tag_compilation:
            dest_dir = self.config.get(sect, 'dest_dir',
                    vars=self.config_vars) + '\\' + \
                    self.config.get('general', 'dest_dirpattern_comp',
                            vars = self.config_vars)
        else:
            dest_dir = self.config.get(sect, 'dest_dir',
                    vars=self.config_vars) + '\\' + \
                    self.config.get('general', 'dest_dirpattern',
                            vars = self.config_vars)

        if exists(dest_dir) and not isdir(dest_dir):
            print "Error: Existing non-directory path", dest_dir
            sys.exit(1)

        if self.tag_compilation:
            dest_pattern = self.config.get('general', 'dest_imgpattern_comp',
                    vars=self.config_vars)
        else:
            dest_pattern = self.config.get('general', 'dest_imgpattern',
                    vars=self.config_vars)

        dest_file = abspath(join(dest_dir, dest_pattern + ext))

        cli = self.config.get('programs', exe) + ' ' + \
                self.config.get(sect, 'args', vars=self.config_vars)

        # Generating temporary files
        win32_exec(cli, self.options.verbose)

        if self.config.get(sect, 'copy_cue') == 'Yes':
            tmp_cue = mktemp('.cue', 'pp_', self.wd)
            morph_cue(self.src_cuefile, tmp_cue, basename(dest_file))

        # Applying replaygain
        self.config_vars['f_infile'] = tmp_dest
        rg_args = self.config.get(sect, 'replay_gain_args', \
                vars=self.config_vars)
        if rg_args:
            cli = self.config.get('programs', rgexe) + ' ' + rg_args
            win32_exec(cli, self.options.verbose)

        # Moving temp to final
        if not exists(dest_dir):
            os.makedirs(dest_dir)

        print
        print "Copying files into", dest_dir

        shutil.move(tmp_dest, dest_file)

        if self.config.get(sect, 'copy_cue') == 'Yes':
            dest_cue = splitext(dest_file)[0] + '.cue'
            shutil.move(tmp_cue, dest_cue)

        if self.config.get(sect, 'copy_log') == 'Yes':
            if not isfile(self.src_logfile) and self.options.eac:
            # try to find the log file again
                log_list = fnmatch.filter(os.listdir(self.src_dir), '*.log')
                if len(log_list) == 0:
                    self.src_logfile = ""
                else:
                    self.src_logfile = join(self.src_dir, log_list[0])
            if isfile(self.src_logfile):
                dest_log = splitext(dest_file)[0] + '.log'
                shutil.copyfile(self.src_logfile, dest_log)


        print 'Files copied OK.'
        print


    def encode_tracks(self, type):
        ext = '.' + type               # file extension
        sect = type + '_tracks'        # config section
        exe = type + '_exe'            # setting name for codec executable
        rgexe = type + '_rgexe'        # setting name for replaygain executable

        wd_files = os.listdir(self.wd)
        wd_files.sort()
        src_files = fnmatch.filter(wd_files, '*.wav')
        tmp_files = []

        for src_file in src_files:
            n = int(splitext(src_file)[0])
            self.config_vars['track_number'] = str(n)
            self.config_vars['track_artist'] = self.tag_tracks[n-1]['artist']
            self.config_vars['track_title'] = self.tag_tracks[n-1]['title']
            self.config_vars['f_track_number'] = \
                    fixfilename(str(n).rjust(2, '0'))
            self.config_vars['f_track_artist'] = \
                    fixfilename(self.tag_tracks[n-1]['artist'])
            self.config_vars['f_track_title'] = \
                    fixfilename(self.tag_tracks[n-1]['title'])
            self.config_vars['f_infile'] = \
                    abspath(os.path.join(self.wd, src_file))
            self.config_vars['f_outfile'] = \
                    splitext(self.config_vars['f_infile'])[0] + ext

            tmp_files.append(self.config_vars['f_outfile'])

            cli = self.config.get('programs', exe) + ' ' + \
                    self.config.get(sect, 'args', vars=self.config_vars)

            win32_exec(cli, self.options.verbose)

        self.config_vars['f_infile_list'] = ''
        for tmp_file in tmp_files:
            self.config_vars['f_infile_list'] += '"' + tmp_file + '" '
        rg_args = self.config.get(sect, 'replay_gain_args', \
                vars=self.config_vars)
        if rg_args:
            cli = self.config.get('programs', rgexe) + ' ' + rg_args
            win32_exec(cli, self.options.verbose)

        if self.tag_compilation:
            dest_dir = self.config.get(sect, 'dest_dir',
                    vars=self.config_vars) + '\\' + \
                    self.config.get('general', 'dest_dirpattern_comp',
                            vars = self.config_vars)
        else:
            dest_dir = self.config.get(sect, 'dest_dir',
                    vars=self.config_vars) + '\\' + \
                    self.config.get('general', 'dest_dirpattern',
                            vars = self.config_vars)

        if exists(dest_dir) and not isdir(dest_dir):
            print "Error: Existing non-directory path", dest_dir
            sys.exit(1)

        print
        print "Copying files into", dest_dir

        for tmp_file in tmp_files:
            n = int(splitext(basename(tmp_file))[0])
            self.config_vars['track_number'] = str(n)
            self.config_vars['track_artist'] = self.tag_tracks[n-1]['artist']
            self.config_vars['track_title'] = self.tag_tracks[n-1]['title']
            self.config_vars['f_track_number'] = \
                    fixfilename(str(n).rjust(2, '0'))
            self.config_vars['f_track_artist'] = \
                    fixfilename(self.tag_tracks[n-1]['artist'])
            self.config_vars['f_track_title'] = \
                    fixfilename(self.tag_tracks[n-1]['title'])

            if self.tag_compilation:
                dest_pattern = self.config.get('general', 'dest_trkpattern_comp',
                        vars=self.config_vars)
            else:
                dest_pattern = self.config.get('general', 'dest_trkpattern',
                        vars=self.config_vars)

            dest_file = abspath(join(dest_dir, dest_pattern + ext))

            if not exists(dest_dir):
                os.makedirs(dest_dir)

            print "    ", basename(dest_file)
            shutil.move(tmp_file, dest_file)

        if self.tag_compilation:
            dest_spattern = self.config.get('general', 'dest_imgpattern_comp',
                    vars=self.config_vars)
        else:
            dest_spattern = self.config.get('general', 'dest_imgpattern',
                    vars=self.config_vars)

        if self.config.get(sect, 'copy_cue') == 'Yes':
            dest_cue = abspath(join(dest_dir, dest_spattern + '.cue'))
            print "    ", basename(dest_cue)
            shutil.copyfile(self.src_cuefile, dest_cue)

        if self.config.get(sect, 'copy_log') == 'Yes':
            if not isfile(self.src_logfile) and self.options.eac:
            # try to find the log file again
                log_list = fnmatch.filter(os.listdir(self.src_dir), '*.log')
                if len(log_list) == 0:
                    self.src_logfile = ""
                else:
                    self.src_logfile = join(self.src_dir, log_list[0])
            if isfile(self.src_logfile):
                dest_log = abspath(join(dest_dir, dest_spattern + '.log'))
                print "    ", basename(dest_log)
                shutil.copyfile(self.src_logfile, dest_log)

        if type == 'flac' and self.config.get(sect, 'use_md5') == 'Yes':
            print "Creating MD5 file...",
            dest_md5 = abspath(join(dest_dir, dest_spattern + '.md5'))
            md5_file = open(dest_md5, 'wt')
            img2md5 = open(self.src_imgfile, 'rb')
            md5_file.write(binascii.b2a_hex(md5.new(img2md5.read()).digest()) + '  ' + dest_spattern + '.wav\n')
            img2md5.close()
            md5_file.close()
            print "done."


        print "Files copied OK."
        print


    def split_image(self):
        cli = self.config.get('programs', 'acdir_exe')
        cli += ' --hidden-track1 -o "' + self.wd + '\%n.wav"'
        cli += ' --extract "' + self.src_cuefile + '"'
        win32_exec(cli, self.options.verbose)


def main(argv=None):
    print _PANPIPE_LICENSE_BLURB

    if argv is None:
        argv = sys.argv

    app = PPApp()

    return 0


if __name__ == "__main__":
    rc = main()
    sys.exit(rc)

